Skip to content

[HDX-4406] Number tile conditional color rules (ordered thresholds)#2386

Draft
alex-fedotyev wants to merge 7 commits into
mainfrom
alex/HDX-4406-number-tile-conditional-color-rules
Draft

[HDX-4406] Number tile conditional color rules (ordered thresholds)#2386
alex-fedotyev wants to merge 7 commits into
mainfrom
alex/HDX-4406-number-tile-conditional-color-rules

Conversation

@alex-fedotyev
Copy link
Copy Markdown
Contributor

@alex-fedotyev alex-fedotyev commented May 30, 2026

Summary

This PR adds conditional color rules to number tiles. Define an ordered list of conditions; the last matching rule's color wins — so list rules in ascending priority order with the highest-priority rule last. If no rule matches, the tile falls back to its static color (set in the previous AC3 PR), then to the default text color.

The schema is intentionally generic at the shared level so a future table-tile slice can attach per-column rules without a schema change.

What changed

common-utils (schema)

  • Added ColorConditionSchema (discriminated union of gt, gte, lt, lte, between, eq, neq, contains, startsWith, endsWith, regex operators)
  • Added colorRules (optional, max 10) to SharedChartSettingsSchema alongside the existing color field

app (resolver)

  • evaluateColorCondition: evaluates a single rule against a runtime value with proper type guards (numeric ops reject strings, string ops reject numbers, bad regex returns false)
  • resolveConditionalColor: iterates rules in order, last match wins, falls back to fallback (the static color) when nothing matches
  • String data values (ClickHouse returns UInt64 counts as JSON strings) are coerced to numbers before rule evaluation

app (UI)

  • New ColorRulesEditor component: sortable rule list via @dnd-kit/sortable, per-operator value inputs (single number, two-number range for between, text-or-number for eq/neq), ColorSwatchInput per rule, add/delete buttons; "Add rule" disables at 10
  • ChartDisplaySettingsDrawer: added "Conditional colors" section gated on DisplayType.Number, placed below the existing static color picker
  • EditTimeChartForm: colorRules wired through useWatch, displaySettings memo, and handleUpdateDisplaySettings
  • DBNumberChart: resolves color via resolveConditionalColor(rawValue, config.colorRules, config.color) at render time

Tests

  • Schema: positive + negative cases for all operators and array length constraints
  • Resolver: evaluateColorCondition per-operator, resolveConditionalColor including last-match-wins, string coercion, and the canonical success/warning/error scenario
  • UI: add/delete/operator shape/color swatch/render cases for ColorRulesEditor
  • Integration: DBNumberChart with color: 'chart-success' + two rules confirms value 50 -> success, 200 -> warning, 1000 -> error; also covers string UInt64 input

No changes to packages/api schemas or external API (separate follow-up ticket, mirrors HDX-4378).

Screenshots or video

Before After
Number tile has only a static color picker Number tile shows a "Conditional colors" section with add/reorder/delete rule controls below the static color picker

How to test on Vercel preview

Preview routes: /dashboards

Steps:

  1. Open or create a dashboard and add a number tile (or edit an existing one).
  2. Click the "Display Settings" button to open the drawer.
  3. Confirm the "Conditional colors" section appears below the "Color" picker.
  4. Click "Add rule" and verify a rule row appears with operator >, a value input, a color swatch, and a delete button.
  5. Change the operator to >= and set value to 100; pick the Warning color swatch.
  6. Click "Add rule" again; set operator >=, value 500; pick the Error color swatch.
  7. Click "Apply". Verify the tile persists the rules (re-open Display Settings and confirm the two rules are still there).
  8. With a tile value below 100, confirm it renders in the static (or default) color.
  9. With a value between 100 and 499, confirm warning color.
  10. With a value >= 500, confirm error color.
  11. Click "Add rule" 8 more times to reach 10 rules total. Verify the button becomes disabled.
  12. Drag a rule handle to reorder and click Apply; confirm the new order is reflected on re-open.

References

Alex Fedotyev added 5 commits May 29, 2026 23:35
Per-row Tooltip.Floating instances got stranded in their Portal when a
virtual row unmounted before onMouseLeave fired — a race that occurs
when the mouse moves rapidly across a TanStack Virtual list.

Fix: replace one Tooltip.Floating per virtual row with a single shared
Tooltip.Floating wrapping the whole <tbody>. The floating tooltip now
lives on <tbody>, which never unmounts, so its Portal-rendered content
can never be left open after the triggering element disappears.

Row-level onMouseEnter/onMouseLeave handlers update a shared
hoveredRowDescription state; the tooltip's disabled prop gates
visibility so rows without a resolved URL (error-toast branch) never
show a hint. A tbody-level onMouseLeave acts as a safety net to clear
the description if a rapid mouse move causes a row to unmount before its
own leave handler fires.

Test: adds a regression test that verifies the tooltip disappears on
mouseLeave (the stranded-tooltip scenario).
Verifies that the Tooltip.Floating hint appears on row hover and
disappears when the mouse moves away, covering the stranded-tooltip
regression introduced by the per-row Tooltip.Floating in PR #2321.

Changes:
- dashboard-table-linking.spec.ts: new 'Tooltip hint appears on hover
  and disappears on mouse-leave' test. Creates a table tile with a
  Search row-click action, hovers the first row to confirm the hint
  appears, then moves the mouse away to confirm it hides cleanly.
- DashboardPage.ts: adds getFirstTableRow() and
  hoverFirstTableRowAndGetTooltip() page-object helpers.
P2 — correctness:
- Store hoveredVirtualIndex (row index) instead of hoveredRowDescription
  (string) so the label re-derives via useMemo on every render. If the
  virtualiser drops or replaces the hovered row (scroll, auto-refetch,
  rapid cursor movement) the new row's action is shown immediately;
  stale text from the unmounted row can never persist.
- Rows with url:null or empty description now correctly disable the
  tooltip regardless of what the prior hover state was.

P2 — testing:
- Replaced the simple mouseLeave regression test with one that exercises
  the actual race: hover index 0 (URL row), then enter index 1 (no-URL
  row) without firing mouseLeave on index 0. Asserts tooltip hides by
  inspecting the Mantine inline display style on the Portal container.

P3 — maintainability:
- label={hoveredRowDescription} — drop the ?? '' fallback that obscured
  the disabled gating relationship (Mantine accepts null as ReactNode).
- disabled={!hoveredRowDescription} — guards against empty-string
  descriptions that would mount a zero-width floating tooltip.
- Unconditional onMouseEnter/onMouseLeave on each <tr> with a hoisted
  clearHovered useCallback, replacing the conditional handler pattern
  that forked JSX unnecessarily.
- Collapse dual comment blocks into one rationale at the Tooltip.Floating
  call site; the state declaration now has a single-line pointer.
- Add data-testid="row-action-hint" to the Tooltip.Floating label span
  so E2E tests locate the tooltip by stable testid rather than by
  hard-coupled copy strings.
Add Grafana-style threshold color rules to number tiles. Users can
define an ordered list of conditions in Display Settings; the last
matching rule's color wins, falling back to the static tile color,
then to the default text color.

Schema (common-utils):
- Add ColorConditionSchema (discriminated union: gt/gte/lt/lte/between/
  eq/neq/contains/startsWith/endsWith/regex)
- Add colorRules to SharedChartSettingsSchema (optional, max 10)

Resolver (app):
- evaluateColorCondition: per-rule evaluation with type guards
- resolveConditionalColor: last-match-wins, null-safe

UI (app):
- ColorRulesEditor: sortable rule list via @dnd-kit/sortable,
  per-operator value inputs, ColorSwatchInput, add/delete controls
- ChartDisplaySettingsDrawer: conditional colors section gated on
  DisplayType.Number, below existing static color picker
- EditTimeChartForm: wire colorRules through displaySettings and
  handleUpdateDisplaySettings
- DBNumberChart: evaluate rules against raw numeric value at render time

Tests:
- Schema positive/negative tests (common-utils)
- evaluateColorCondition + resolveConditionalColor unit tests (app)
- ColorRulesEditor RTL tests (add/delete/operator/render)
- DBNumberChart integration test: success/warning/error scenario

No API schema or external-API changes (separate follow-up ticket).
@alex-fedotyev alex-fedotyev added the ai-generated AI-generated content; review carefully before merging. label May 30, 2026
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 30, 2026

⚠️ No Changeset found

Latest commit: a5d93b8

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@vercel
Copy link
Copy Markdown

vercel Bot commented May 30, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
hyperdx-oss Ready Ready Preview, Comment May 30, 2026 4:28am
hyperdx-storybook Error Error May 30, 2026 4:28am

Request Review

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 30, 2026

E2E Test Results

All tests passed • 194 passed • 3 skipped • 1272s

Status Count
✅ Passed 194
❌ Failed 0
⚠️ Flaky 2
⏭️ Skipped 3

Tests ran across 4 shards in parallel.

View full report →

…yout

Two fixes:

1. Color not applying: ClickHouse returns UInt64 counts as JSON strings
   when output_format_json_quote_64bit_integers=1. The rawValue passed to
   resolveConditionalColor was a string like "1251132", causing all
   numeric operators (gte, gt, lt, etc.) to short-circuit with a false
   result. Coerce parseable-as-number strings to numbers before evaluation.
   Test added for the string-coercion path.

2. Layout: rule rows now use fixed widths and center alignment instead of
   stretching full-width with flex-start offsets. Operator w=80,
   value w=120 (single) / w=72 each (between), Add rule button left-aligned.
@alex-fedotyev alex-fedotyev changed the title [HDX-4406] Number tile conditional color rules (Grafana-style thresholds) [HDX-4406] Number tile conditional color rules (ordered thresholds) May 30, 2026
Replace references to a competitor's threshold model with neutral
terminology: 'last-match-wins' and 'higher-priority rules go last'.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ai-generated AI-generated content; review carefully before merging.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant